using System;
using System.Text;
using System.Data;
using System.Collections;
using System.Collections.Generic;

using Framework;
using System.IO;
using System.Drawing;
using System.Threading.Tasks;

namespace SchemaMonitor
{
    public enum EImgBucket
    {
        None = 0,
        Laptop = 1,
        XRated = 2,
        Deleted = 3,
        Unknown = 10
    }

	//Table-Mapping Class (Customisable half)
	public partial class CPhotoHash
	{

		#region Constants
		public const string VIEW_NAME_FULL = "vwPhotoHash";
		#endregion

		#region Constructors (Public)
		//Default Connection String
		public CPhotoHash() : base() { }
		public CPhotoHash(CPhotoHash orig) : base()
		{
			this.PhotoBlob = orig.PhotoBlob;
			this.PhotoCreated = orig.PhotoCreated;
			this.PhotoHeight = orig.PhotoHeight;
			this.PhotoWidth = orig.PhotoWidth;
			this.PhotoMd5 = orig._photoMd5;
			this.PhotoParentMD5 = orig._photoParentMD5;
			this.PhotoCaptions = orig.PhotoCaptions;
			this.PhotoSize = orig.PhotoSize;
		}

		//Alternative Connection String
		public CPhotoHash(CDataSrc dataSrc) : base(dataSrc) { }

		//Hidden  (UI code should use cache instead)
		protected internal CPhotoHash(Guid photoMd5) : base(photoMd5) { }
		protected internal CPhotoHash(CDataSrc dataSrc, Guid photoMd5) : base(dataSrc, photoMd5) { }
		protected internal CPhotoHash(CDataSrc dataSrc, Guid photoMd5, IDbTransaction txOrNull) : base(dataSrc, photoMd5, txOrNull) { }
		#endregion

		#region Default Values
		protected override void InitValues_Custom()
		{
			//_sampleDateCreated = DateTime.Now;
			//_sampleSortOrder   = 0;
		}
		#endregion

		#region Default Connection String
		protected override CDataSrc DefaultDataSrc() { return CConfig.Photos; } //CDataSrc.Default;  }
		#endregion

		#region Properties - Relationships
		//Relationships - Foriegn Keys (e.g parent)

		//Relationships - Collections (e.g. children)
		public CChapterPictureList ChapterPictures { get { return CChapterPicture.Cache.GetByPhotoMD5(this.PhotoMd5); } }
		protected CChapterPictureList ChapterPictures_(IDbTransaction tx) { return new CChapterPicture(DataSrc).SelectByPhotoMD5(this.PhotoMd5, tx); } //Only used for cascade deletes

		public CPhotoList Photos { get { return CPhoto.Cache.GetByMd5(this.PhotoMd5); } }
		protected CPhotoList Photos_(IDbTransaction tx) { return new CPhoto(DataSrc).SelectByMd5(this.PhotoMd5, tx); } //Only used for cascade deletes


		public CLabelList Labels { get { return CLabel.Cache.GetByGuid(this.PhotoMd5); } }
		protected CLabelList Labels_(IDbTransaction tx) { return new CLabel(DataSrc).SelectByGuid(this.PhotoMd5, tx); } //Only used for cascade deletes

		public CLabel_OneToOneList Label_OneToOnes { get { return CLabel_OneToOne.Cache.GetByGuid(this.PhotoMd5); } }
		protected CLabel_OneToOneList Label_OneToOnes_(IDbTransaction tx) { return new CLabel_OneToOne(DataSrc).SelectByGuid(this.PhotoMd5, tx); } //Only used for cascade deletes

		//2-step
		public CChapterList Chapters { get { return ChapterPictures.Chapters; } }

		public CMetaTagList MetaTags { get { return Labels.MetaTags; } }
		public CMetaTagList RemainingMetaTags(string search) { return Labels.RemainingMetaTags(search); }

		public CMetaTagList MetaTagsO2O { get { return Label_OneToOnes.MetaTags; } }
		public CMetaTagList RemainingMetaTagsO2O(string search) { return Label_OneToOnes.RemainingMetaTags(search); }
		#endregion

		#region Properties - Customisation
		public int Usage { get { return ChapterPictures.Count; } }
		public bool InUse { get { return Usage > 0; } }
		public int TagCount { get { return Labels.Count; } }
		public bool Tagged { get { return TagCount > 0; } }

        public bool AddLabel(int tagId) { return AddLabel(CMetaTag.Cache.GetById(tagId)); }
        public bool AddLabel(CMetaTag tag)
        {
            if (null == tag)
                throw new Exception("Invalid TagId");

            if (tag.TagType.TagIsOneToOne)
                throw new Exception("Wrong tag-type");

            var lbl = CLabel.Cache.GetById(this.PhotoMd5, tag.TagId);
            if (null != lbl)
                return false;

            lbl = new CLabel();
            lbl.LabelGuid = this.PhotoMd5;
            lbl.LabelTagId = tag.TagId;
            lbl.Save();
            return true;
        }
        public bool RemoveLabel(CMetaTag tag)
        {
            if (null == tag) return false;
            var lbl = CLabel.Cache.GetById(this.PhotoMd5, tag.TagId);
            if (null == lbl) return false;
            lbl.Delete();
            return true;
        }
        public EImgBucket Bucket
        {
            get
            {
                var buck = CLabel_OneToOne.Cache.GetById(this.PhotoMd5, CTagType.Cache.FB_Filters.TypeId);
                if (null == buck)
                    return EImgBucket.None;
                if (buck.LabelTagId == CMetaTag.Cache.Laptop.TagId)
                    return EImgBucket.Laptop;
                if (buck.LabelTagId == CMetaTag.Cache.Delete.TagId)
                    return EImgBucket.Deleted;
                if (buck.LabelTagId == CMetaTag.Cache.XRated.TagId)
                    return EImgBucket.XRated;
                return EImgBucket.Unknown;
            }
            set
            {
                var buck = CLabel_OneToOne.Cache.GetById(this.PhotoMd5, CTagType.Cache.FB_Filters.TypeId);
                if (value == EImgBucket.None)
                {
                    if (buck != null)
                        buck.Delete();
                    return;
                }
                if (buck == null)
                {
                    buck = new CLabel_OneToOne();
                    buck.LabelGuid = this.PhotoMd5;
                    buck.LabelTypeId = CTagType.Cache.FB_Filters.TypeId;
                }
                var tagId = 0;
                if (value == EImgBucket.Deleted)
                    tagId = CMetaTag.Cache.Delete.TagId;
                else if (value == EImgBucket.Laptop)
                    tagId = CMetaTag.Cache.Laptop.TagId;
                else if (value == EImgBucket.XRated)
                    tagId = CMetaTag.Cache.XRated.TagId;
                else
                    throw new Exception("Unknown Tag: " + value.ToString());

                if (buck.LabelTagId != tagId)
                {
                    buck.LabelTagId = tagId;
                    buck.Save();
                }
            }
        }


		//Derived/ReadOnly (e.g. xml classes, presentation logic)
		public string Size_ { get { return CUtilities.FileSize(PhotoSize); } }

		public byte[] GetBlob() { return GetBlob(DataSrc); }
		public byte[] GetBlob(CDataSrc db)
		{
			var sql = string.Concat("SELECT PhotoBlob FROM ", TABLE_NAME, " WHERE ", PRIMARY_KEY_NAME, "=@md5");
			var cmd = new CCommand(sql, new CNameValueList("md5", this.PhotoMd5), false);
			var obj = db.ExecuteScalar(cmd);
			if (obj is byte[])
				return (byte[])obj;
			return null;
		}
		public List<Int16> GetThumbAsList()
		{
			var gz = GetThumb();
			if (null == gz) return null;
			var bin = CBinary.Unzip(gz);
			return CProto.Deserialise<List<Int16>>(bin);
		}
		public byte[] GetThumb()
		{
			var sql = string.Concat("SELECT PhotoThumb48x48_Int16ProtoGz FROM ", TABLE_NAME, " WHERE ", PRIMARY_KEY_NAME, "=@md5");
			var cmd = new CCommand(sql, new CNameValueList("md5", this.PhotoMd5), false);
			var obj = DataSrc.ExecuteScalar(cmd);
			if (obj is byte[])
				return (byte[])obj;
			return null;
		}
		#endregion

		#region Save/Delete Overrides
		//Can Override base.Save/Delete (e.g. Cascade deletes, or insert related records)
		#endregion

		protected override CNameValueList ColumnNameValues()
		{
			CNameValueList data = new CNameValueList();
			data.Add("PhotoMd5", CAdoData.NullVal(_photoMd5));
			data.Add("PhotoParentMD5", CAdoData.NullVal(_photoParentMD5));
			if (null != _photoBlob)
				data.Add("PhotoBlob", CAdoData.NullVal(_photoBlob));
			data.Add("PhotoWidth", CAdoData.NullVal(_photoWidth));
			data.Add("PhotoHeight", CAdoData.NullVal(_photoHeight));
			data.Add("PhotoCreated", CAdoData.NullVal(_photoCreated));
			data.Add("PhotoSize", CAdoData.NullVal(_photoSize));
			data.Add("PhotoCaptions", CAdoData.NullVal(_photoCaptions));
			data.Add("PhotoFileNames", CAdoData.NullVal(_photoFileNames));
			if (null != _photoThumb48x48_Int16ProtoGz)
				data.Add("PhotoThumb48x48_Int16ProtoGz", CAdoData.NullVal(_photoThumb48x48_Int16ProtoGz));
			data.Add("PhotoThumbMD5", CAdoData.NullVal(_photoThumbMD5));
			return data;
		}

		#region Custom Database Queries
		//(Not normally required for cached classes, use list class for searching etc)
		//For Stored Procs can use: MakeList (matching schema), or DataSrc.ExecuteDataset (reports etc)
		//For Dynamic sql, can use: SelectSum, SelectDistinct, SelectCount, SelectWhere (inherited methods)
		#endregion

		#region Searching (Optional)
		//For cached classes, custom seach logic resides in static methods on the list class
		// e.g. CPhotoHash.Cache.Search("...")

		//See also the auto-generated methods based on indexes
		//' e.g. CPhotoHash.Cache.GetBy...
		#endregion

		#region Caching Details
		//Cache Key
		internal static string CACHE_KEY = typeof(CPhotoHash).ToString();    //TABLE_NAME

		//Cache data
		private static CPhotoHashList LoadCache() { return new CPhotoHash().SelectAll(); }
		//Cache Timeout
		private static void SetCache(CPhotoHashList value)
		{
			if (null != value)
				value.Sort();
			CCache.Set(CACHE_KEY, value);    //Optional parameter can override timeout (otherwise uses config-settings, which default to 3hrs)
		}
		//Helper Method
		private CPhotoHash CacheGetById(CPhotoHashList list) { return list.GetById(this.PhotoMd5); }
		#endregion

		#region Cloning
		public CPhotoHash Clone(CDataSrc target, IDbTransaction txOrNull) //, int parentId)
		{
			//Shallow copy: Copies the immediate record, excluding autogenerated Pks
			CPhotoHash copy = new CPhotoHash(this, target);

			//Deep Copy - Child Entities: Cloned children must reference their cloned parent
			//copy.SampleParentId = parentId;

			copy.Save(txOrNull);

			//Deep Copy - Parent Entities: Cloned parents also clone their child collections
			//this.Children.Clone(target, txOrNull, copy.PhotoMd5);

			return copy;
		}
		#endregion

		#region ToXml
		protected override void ToXml_Custom(System.Xml.XmlWriter w)
		{
			//Store(w, "Example", this.Example)
		}
		#endregion


		#region Static
		//Get Thumbs as dict
		public const int THREADS_Check4NewThumbs = -1;

		public static void SetThumbs(Dictionary<Guid, List<Int16>> d)
		{
			var path = CConfig.ThumbsPath;
			File.WriteAllBytes(path, CProto.Serialise(d));
			if (File.Exists(CConfig.ThumbsPath_Old))
				File.Delete(CConfig.ThumbsPath_Old);
		}
		public static Dictionary<Guid, List<Int16>> GetThumbs(CDataSrc db = null, int threads = THREADS_Check4NewThumbs)
		{
			var d = GetThumbs_FileOrDb(db);
			Console.WriteLine("Read " + d.Count.ToString("n0") + " thumbs. Generating missing thumbs...");

			var po = new ParallelOptions();
			po.MaxDegreeOfParallelism = threads;

			var before = d.Count;
			Parallel.ForEach(CPhotoHash.Cache, po, ph =>
			{
				if (!d.ContainsKey(ph.PhotoMd5))
				{
					var rgb = ph.GetThumbAsList();
					if (null == rgb)
					{
						try
						{
							var png = ph.GetBlob();
							rgb = CPhotoHash.Image2Thumb(png);
							var bin = CProto.Serialise(rgb);
							var gz = CBinary.Zip(bin);
							ph.PhotoThumb48x48_Int16ProtoGz = gz;
							ph.PhotoThumbMD5 = CBinary.MD5_(gz);
							ph.Save();
						}
						catch
						{; }
					}
					if (null != rgb)
						lock (d) { d.Add(ph.PhotoMd5, rgb); }
				}
			});


			if (d.Count > before)
			{
				Console.WriteLine("generated " + (d.Count - before).ToString("n0") + " new thumbs");
				return SaveThumbs(d);
			}
			Console.WriteLine("Loaded " + CUtilities.CountSummary(d.Count, "thumb"));
			return d;
		}
		public static Dictionary<Guid, List<Int16>> GetThumbs_FileOrDb(CDataSrc db = null)
		{
			if (File.Exists(CConfig.ThumbsPath))
			{
				var blob = File.ReadAllBytes(CConfig.ThumbsPath);
				return CProto.Deserialise<Dictionary<Guid, List<Int16>>>(blob);
			}
			else if (File.Exists(CConfig.ThumbsPath_Old))
			{
				var blob = CBinary.Unzip(File.ReadAllBytes(CConfig.ThumbsPath_Old));
				return CProto.Deserialise<Dictionary<Guid, List<Int16>>>(blob);
			}
			return SaveThumbs(GetThumbs_FromDb(db));
		}
		private static Dictionary<Guid, List<Int16>> SaveThumbs(Dictionary<Guid, List<Int16>> d)
		{
			File.WriteAllBytes(CConfig.ThumbsPath, CProto.Serialise(d));
			return d;
		}
		public static Dictionary<Guid, List<Int16>> GetThumbs_FromDb(CDataSrc db = null)
		{
			var dt = GetThumbsFromDb(db);
			var d = new Dictionary<Guid, List<Int16>>();
			foreach (DataRow i in dt.Rows)
			{
				var md5 = CAdoData.GetGuid(i, 0);
				var gz = CAdoData.GetBytes(i, 1);
				var bin = CBinary.Unzip(gz);
				d.Add(md5, CProto.Deserialise<List<Int16>>(bin));
			}
			return d;
		}
		private static DataTable GetThumbsFromDb(CDataSrc db = null)
		{
			db = db ?? CConfig.Photos;
			var sql = "SELECT PhotoMd5, PhotoThumb48x48_Int16ProtoGz FROM PhotoHash";
			return db.ExecuteDataSet(new CCommand(sql)).Tables[0];
		}

		//Image2Thumb
		public static List<Int16> Image2Thumb(byte[] blob)
		{
			var ms = new MemoryStream(blob);
			var img = Image.FromStream(ms);
			int w = 0;
			int h = 0;
			return Image2Thumb(img, ref w, ref h);
		}
		public static List<Int16> Image2Thumb(string path, ref int width, ref int height)
		{
			var img = Image.FromFile(path);
			return Image2Thumb(img, ref width, ref height);
		}
		private static List<Int16> Image2Thumb(Image img, ref int width, ref int height)
		{
			var thm = img.GetThumbnailImage(48, 48, null, new IntPtr());
			var bmp = new Bitmap(thm);
			var rgb = new List<Int16>(48 * 48);
			for (var i = 0; i < 48; i++)
				for (var j = 0; j < 48; j++)
				{
					var px = bmp.GetPixel(i, j);
					int xx = (Int16)px.R + (Int16)px.G + (Int16)px.B;
					rgb.Add(Convert.ToInt16(xx));
				}
			bmp.Dispose();
			width = img.Width;
			height = img.Height;
			img.Dispose();
			return rgb;
		}

		//Overloads
		public static byte[] Image2ThumbGz(string path, ref int width, ref int height)
		{
			return CBinary.Zip(CProto.Serialise(Image2Thumb(path, ref width, ref height)));
		}
		public static byte[] Image2ThumbGz(byte[] blob)
		{
			return CBinary.Zip(CProto.Serialise(Image2Thumb(blob)));
		}


		//Correlate
		public static void FindAndStoreMatches(int decisionLevel, bool validate = true, int threads = -1, CDataSrc from = null, CDataSrc to = null)
		{
			from = from ?? CConfig.Photos;
			to = to ?? CConfig.Photos;

			var po = new ParallelOptions();
			po.MaxDegreeOfParallelism = threads;


			//Previous (or Active) matches
			var activeMatches = new CImageMatching(to).SelectAll();
			Console.WriteLine("Found " + CUtilities.CountSummary(activeMatches, "match", "no existing matchs"));

			var checkedMatches = new CImageMatchedButDifferent(to).SelectAll();
			Console.WriteLine("Found " + CUtilities.CountSummary(checkedMatches, "false positive", "no existing matchs"));

			//Known thumbs
			var dict = GetThumbs();

			//Optional step
			List<Guid> valid = null;
			if (validate)
				valid = Validate(to, activeMatches, checkedMatches, dict);
			else
				valid = CPhotoHash.Cache.Ids;

            //Threadsafe copy
            //var shuffled = new List<Guid>(valid.Count);
            //foreach (Guid i in CShuffle.Shuffle(valid))
            //	shuffled.Add(i);
            var sorted = new List<Guid>(valid);
            sorted.Sort();
            valid = null;


            //Outerloop = single thread
            int counter = 0;
			foreach (Guid g1 in sorted)
			{
				//Get the list of done ones
				Dictionary<Guid, int> d = GetCorr(g1);

				//Convert guid to numbers
				var b1 = g1.ToByteArray();
				var id1a = BitConverter.ToInt64(b1, 0);
				var id1b = BitConverter.ToInt64(b1, 8);

				var found = new List<int>();
				var computed = 0;
				counter++;

				if (new Random().Next(1, 100) == 50)
					Console.WriteLine(CImageMatching.Progress<Guid>(sorted, g1));

				//Inner loop = multi-threaded
				Parallel.ForEach<Guid>(sorted, po, g2 =>
				{
					try
					{
						//Same image
						if (g1 == g2)
							return;

                        //Only do Half
                        var b2 = g2.ToByteArray();
                        var id2a = BitConverter.ToInt64(b2, 0);
                        if (id1a > id2a)
                        {
                            if (d.ContainsKey(g2))
                                d.Remove(g2);
                            return;
                        }
                        if (id1a == id2a)
                        {
                            var id2b = BitConverter.ToInt64(b2, 8);
                            if (id2a > id2b)
                            {
                                if (d.ContainsKey(g2))
                                    d.Remove(g2);
                                return;
                            }
                        }

                        //Precomputed
                        int x = -1;
						try
						{
                            if (d.ContainsKey(g2))
                            {
                                x = d[g2];
                                if (x > decisionLevel)
                                    return;
                            }
						}
						catch //threading bug
                        {
                            if (d.ContainsKey(g2))
                            {
                                x = d[g2];
                                if (x > decisionLevel)
                                    return;
                            }
                        }


						//Compute
						if (-1 == x)
						{
							x = Corr(dict[g1], dict[g2]);
                            lock (d)
                                d.Add(g2, x);
                            computed++;
						}

						//Report
						if (x < decisionLevel)
						{
							if (null == activeMatches.GetById(g1, g2))
								if (null == checkedMatches.GetById(g1, g2))
									CImageMatching.Store(g1, g2, x, to);

							if (null == activeMatches.GetById(g2, g1))
								if (null == checkedMatches.GetById(g2, g1))
									CImageMatching.Store(g2, g1, x, to);

							found.Add(x);
						}
					}
					catch { }
				});

				if (computed > 0)
				{
					SetCorr(g1, d);
					if (found.Count > 0 || new Random().Next(1, 100) == 50)
						Console.WriteLine(CImageMatching.Progress<Guid>(sorted, g1) + "\t" + counter.ToString("n0") + "\t" + CUtilities.ListToString(found));
				}
			}
		}
		public static List<Guid> Validate(CDataSrc to, CImageMatchingList activeMatches, CImageMatchedButDifferentList checkedMatches, Dictionary<Guid, List<Int16>> dict)
		{
			//Valid (live) ids
			Console.WriteLine("Getting valid ids...");
			var valid = to.MakeListGuid("SELECT PhotoMd5 FROM " + CPhotoHash.TABLE_NAME);
			Console.WriteLine(CUtilities.CountSummary(valid, "valid id"));


			//Delete records if image now gone
			var counter = 0;
			foreach (var i in activeMatches)
				if (!valid.Contains(i.MD5) || !valid.Contains(i.MatchingMd5))
				{
					i.Delete();
					counter++;
				}
			if (counter > 0)
			{
				Console.WriteLine("Deleted " + counter + " matches for deleted images");
				activeMatches = new CImageMatching(to).SelectAll();
			}

			counter = 0;
			foreach (var i in checkedMatches)
				if (!valid.Contains(i.MD5) || !valid.Contains(i.MatchingMd5))
				{
					i.Delete();
					counter++;
				}
			if (counter > 0)
			{
				Console.WriteLine("Deleted " + counter + " matches for inspected images");
				checkedMatches = new CImageMatchedButDifferent(to).SelectAll();
			}


			//Reverse versions
			counter = 0;
			activeMatches.GetById(Guid.Empty, Guid.Empty);
			checkedMatches.GetById(Guid.Empty, Guid.Empty);
			Parallel.ForEach(activeMatches, i =>
			{
				if (null == activeMatches.GetById(i.MatchingMd5, i.MD5))
				{
					CImageMatching.Store(i.MatchingMd5, i.MD5, i.Score, to);
					counter++;
				}
			});
			if (counter > 0)
			{
				Console.WriteLine("Replicated " + counter.ToString("n0"));
				activeMatches = new CImageMatching(to).SelectAll();
			}


			//Remove deleted images
			var before = dict.Count;
			var keys = new List<Guid>(dict.Keys);
			foreach (var i in keys)
				if (!valid.Contains(i))
					dict.Remove(i);
			if (before != dict.Count)
				SaveThumbs(dict);

			return valid;
		}


		private static string XCORR_DIR = CConfig.XCorrFolder;

		private static string CorrFilePath(Guid g)
		{
			var s = g.ToString().Substring(0, 16).ToUpper();
			return string.Concat(XCORR_DIR, s, ".proto.gz");

        }
		private static bool HasCorr(Guid g)
		{
            return File.Exists(CorrFilePath(g));
		}
		public static Dictionary<Guid, int> GetCorr(Guid g)
		{
			if (!HasCorr(g))
				return new Dictionary<Guid, int>(CPhotoHash.Cache.Count);

            var path = CorrFilePath(g);
            try
            {
                var bin = CBinary.Unzip(File.ReadAllBytes(path));
                return CProto.Deserialise<Dictionary<Guid, int>>(bin);
            }
            catch
            {
                File.Delete(path);
                return GetCorr(g);
            }
		}
		private static void SetCorr(Guid g, Dictionary<Guid, int> d)
		{
			if (!Directory.Exists(XCORR_DIR))
                Directory.CreateDirectory(XCORR_DIR);

			File.WriteAllBytes(CorrFilePath(g), CBinary.Zip(CProto.Serialise(d)));
		}

		private static void SetCorr(List<Guid> valid, Guid g, Dictionary<Guid, int> d)
		{
			Trim(valid, d);
			SetCorr(g, d);
		}
		private static void Trim(List<Guid> valid, Dictionary<Guid, int> d)
		{
			var keys = new List<Guid>(d.Keys);
			foreach (var i in keys)
				if (!valid.Contains(i))
					d.Remove(i);
		}




		public static bool HasMatch(Dictionary<Guid, List<Int16>> dict, List<Int16> rgb, int decisionLevel)
		{
			//Copy thumbs (for thread-safe)
			Dictionary<Guid, List<Int16>> copy = null;
			lock (dict) { copy = new Dictionary<Guid, List<Int16>>(dict); }

			//Stop on first match
			foreach (var i in copy)
			{
                var val = i.Value;// dict[i.Key];
				var x = Corr(val, rgb);
				if (x < decisionLevel)
				{
					Console.Write("Matched: " + x.ToString("n0"));
					return true;
				}
			}
			return false;
		}
		public static Dictionary<Guid, int> Match(Dictionary<Guid, List<Int16>> dict, List<Int16> rgb, int decisionLevel)
		{
			//Copy thumbs (for thread-safe)
			Dictionary<Guid, List<Int16>> copy = null;
			lock (dict) { copy = new Dictionary<Guid, List<Int16>>(dict); }

			var matched = new Dictionary<Guid, int>();
			foreach (var i in copy)
			{
				var x = Corr(i.Value, rgb);
				if (x < decisionLevel)
					matched.Add(i.Key, x);
			}
			return matched;
		}
		public static int Corr(List<Int16> v1, List<Int16> v2)
		{
			long x = 0;
			for (var k = 0; k < v1.Count; k++)
				x += Math.Abs((v1[k] - v2[k]));
			return x > int.MaxValue ? int.MaxValue : (int)x;
		}


		//Histogram
		public static Dictionary<Tuple<int, int>, List<Tuple<Guid, Guid>>> FlattenHist(List<Tuple<int, int>> histX, Dictionary<Tuple<int, int>, Dictionary<Guid, List<Guid>>> histY)
		{
			var d = new Dictionary<Tuple<int, int>, List<Tuple<Guid, Guid>>>();
			foreach (var t in histX)
			{
				List<Tuple<Guid, Guid>> list = null;
				if (!d.TryGetValue(t, out list))
				{
					list = new List<Tuple<Guid, Guid>>();
					d.Add(t, list);
				}

				Dictionary<Guid, List<Guid>> d2 = null;
				if (histY.TryGetValue(t, out d2))
				{
					foreach (var gg in d2)
					{
						var g1 = gg.Key;
						foreach (var g2 in gg.Value)
							list.Add(new Tuple<Guid, Guid>(g1, g2));
					}
				}

			}
			return d;
		}
		public static Dictionary<Tuple<int, int>, Dictionary<Guid, List<Guid>>> ConvertCorrToHist(List<Tuple<int, int>> histX, Dictionary<Guid, Dictionary<Guid, int>> xcor)
		{
			var histY = new Dictionary<Tuple<int, int>, Dictionary<Guid, List<Guid>>>(histX.Count);
			foreach (var i in xcor)
				if (null != i.Value)
					Parallel.ForEach(i.Value, j =>
					{
						var g1 = i.Key;
						var g2 = j.Key;
						var x = j.Value;
						foreach (var t in histX)
							if (x >= t.Item1 && x <= t.Item2)
							{
								AddToHist(g1, g2, t, histY);
								break;
							}
					});
			return histY;
		}
		private static void AddToHist(Guid g1, Guid g2, Tuple<int, int> t, Dictionary<Tuple<int, int>, Dictionary<Guid, List<Guid>>> histY)
		{
			Dictionary<Guid, List<Guid>> d1 = null;
			if (!histY.TryGetValue(t, out d1))
			{
				d1 = new Dictionary<Guid, List<Guid>>();
				histY.Add(t, d1);
			}

			List<Guid> list = null;
			if (!d1.TryGetValue(g1, out list))
			{
				list = new List<Guid>(1);
				lock (d1)
				{
					if (!d1.TryGetValue(g1, out list))
						d1.Add(g1, list);
				}
			}
			if (null == list)
			{
				list = new List<Guid>(1);
				d1[g1] = list;
			}
			lock (list)
				list.Add(g2);
		}


		public static List<Tuple<int, int>> HistX(int N = 20)
		{
			var list = new List<Tuple<int, int>>();

			int mid = Int16.MaxValue/2;
			int last = 0;
			for (var i = 1; i <= N; i++)
			{
				double dbl = mid * i; // Math.Pow(2, i);
				int x = Convert.ToInt32(dbl);
				Console.WriteLine(string.Concat(last, "\t *", i, " => \t", x));

				list.Add(new Tuple<int, int>(last, x));
				last = x;
			}
			list.Add(new Tuple<int, int>(last, int.MaxValue));
			return list;
		}
		#endregion
	}
}